//-*- indent-tabs-mode: nil -*-
using System;
using System.Configuration;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;

using System.Text;
using System.Reflection;
using System.Web;

using System.Xml;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
using System.Web.Services.Protocols;

using System.Threading;
using System.Windows.Forms;

namespace seanfoy.oopsnet {
    /// <summary>
    /// This project provides a few different
    /// implementations that match this delegate,
    /// and also lots of boilerplate you need
    /// to get ASP.NET or WPF or whatever to
    /// actually invoke the implementation of your
    /// choice.
    /// </summary>
    public delegate void HandleError(Exception e);

    /// <summary>
    /// Deals with unhandled exceptions by terminating
    /// the process with non-zero exit status
    /// </summary>
    /// <remarks>
    /// Call <see cref="install">install</see> at
    /// application startup.
    /// </remarks>
    public class ConsoleErrorHandler {
        /// <summary>
        /// Set non-zero exit status and write
        /// the exception to stderr
        /// </summary>
        /// <remarks>
        /// You might want to try harder to set an informative
        /// exit status.
        /// <seealso href="http://www.freebsd.org/cgi/man.cgi?query=sysexits&amp;apropos=0&amp;sektion=0&amp;manpath=FreeBSD+4.3-RELEASE&amp;format=html" />
        /// <seealso href="http://www.gnu.org/software/libc/manual/html_node/Exit-Status.html" />
        /// </remarks>
        public static void handleError(Exception e) {
            try {
                Console.Error.WriteLine(e);
            }
            catch {
                //ignore errors secondary to e
            }
            Environment.Exit(1);
        }
        /// <summary>
        /// Unpack args and forward the exception to <see cref="handleError" />
        /// </summary>
        public static void handleAppDomainExceptionImpl(object sender, UnhandledExceptionEventArgs args) {
            handleError(args.ExceptionObject as Exception);
        }
        /// <summary>
        /// Handle events by dispatching to <see cref="handleError" />
        /// (by way of <see cref="handleAppDomainExceptionImpl" />
        /// </summary>
        /// <remarks>
        /// C# 1.0 is verbose, what can I say?
        /// </remarks>
        public static UnhandledExceptionEventHandler handleAppDomainException = new UnhandledExceptionEventHandler(handleAppDomainExceptionImpl);

        /// <summary>
        /// Register to handle exceptions that would otherwise go
        /// unhandled
        /// </summary>
        /// <return>
        /// memento for <see cref="uninstall" />
        /// </return>
        public static object install() {
            AppDomain.CurrentDomain.UnhandledException += handleAppDomainException;
            return handleAppDomainException;
        }

        /// <summary>
        /// Stop handling uncaught exceptions
        /// </summary>
        public static void uninstall(object memento) {
            AppDomain.CurrentDomain.UnhandledException -= (UnhandledExceptionEventHandler)memento;
        }
    }

    /// <summary>
    /// Set a 4XX or 5XX HTTP status and transfer
    /// control to a page that can announce to the
    /// user that something's gone wrong.
    /// </summary>
    public class ASPNetErrorHandler {
        /// <summary>convenience property</summary>
        public static NameValueCollection AppSettings {
            get {
                return
                    #if NET_1_0 || NET_1_1
                    System.Configuration.ConfigurationSettings.AppSettings;
                    #else
                    System.Configuration.ConfigurationManager.AppSettings;
                    #endif
            }
        }

        /// <remarks>
        /// <para>The exception is available
        /// to the error page from the
        /// <see cref="HttpContext.Items" />
        /// using key: <c>seanfoy.oopsnet:exception</c>.</para>
        /// <para>IIS7 in its default configuration
        /// tries to generate its own error pages,
        /// interfering with naive attempts to deal
        /// with errors in a portable way.
        /// Furthermore this configuration is
        /// server-wide and not overridable by
        /// default. This method tries to detect
        /// IIS7 and disable its idiosyncratic
        /// behavior.
        /// <seealso href="http://msdn.microsoft.com/en-us/library/ms689429.aspx" />
        /// <seealso href="http://msdn.microsoft.com/en-us/library/bb763179.aspx" />
        /// <seealso href="http://blog.angrypets.com/2008/03/responsetryskip.html" />
        /// </para>
        /// </remarks>
        public void handleError(Exception e) {
            try {
                e = Preprocess(e);
                Response.Clear();
                Response.StatusCode = httpStatus(e);
                avoidIIS7Interference();
                Context.Items["seanfoy.oopsnet:requestUrl"] = Request.Url;
                Context.Items["seanfoy.oopsnet:referer"] = Request.UrlReferrer;
                Context.Items["seanfoy.oopsnet:exception"] = e;
                string vpath = whereTo();
                Server.ClearError();
                transfer(vpath);
            }
            catch (Exception e2) {
                try {
                    Response.Clear();
                    Response.ContentType = "text/plain";
                    Response.Write(e2.Message + "\n");
                    Response.Write(stringify(e2));
                    Server.ClearError();
                }
                catch {
                    // avoid infinite regress
                }
            }
            catch {
                // avoid infinite regress
            }
        }

        /// <summary>
        /// I prepare <paramref name="e" /> for
        /// triage.
        /// </summary>
        /// <remarks>
        /// ASP.NET wraps unhandled exceptions in
        /// an outer exception that offers no
        /// discernable value to us. I suspect
        /// the purpose of HttpUnhandledException
        /// is to distinguish exceptions in user
        /// code from exceptions in the ASP.NET
        /// machinery itself. Anyway, we must
        /// unpack the contents in order to make
        /// further decisions about the
        /// disposition of the error event.
        /// </remarks>
        public virtual Exception Preprocess(Exception e) {
            // HttpUnhandledException is not very informative.
            if (e is HttpUnhandledException && e.InnerException != null) {
                e = e.InnerException;
            }
            return e;
        }

        private static object [] emptyArgs = new object [] {};
        private static void avoidIIS7Interference() {
            Type responseType = Response.GetType();
            PropertyInfo pi = responseType.GetProperty("TrySkipIisCustomErrors");
            if (pi == null) return;

            pi.SetValue(Response, true, emptyArgs);
        }

        /// <summary>
        /// Set an appropriate status for the HTTP
        /// response, based on the unhandled
        /// exception and taking 500 as a default.
        /// </summary>
        public static int httpStatus(Exception e) {
            HttpException he = e as HttpException;
            return (he != null) ? he.GetHttpCode() : 500;
        }

        public virtual string whereTo() {
            try {
                return AppSettings["seanfoy.oopsnet.errorPagePath"];
            }
            catch (ArgumentNullException) {
                throw new Exception("The appSetting seanfoy.oopsnet.errorPagePath is not set.");
            }
        }

        /// <summary>
        /// Give notice to the user, in a more
        /// human-friendly way than just setting
        /// HTTP status, that there's a problem.
        /// </summary>
        /// <remarks>
        /// <para>If there is an error
        /// processing the error page, we fall
        /// back to a hard-coded plaintext
        /// display.</para>
        /// <para>For some applications, the
        /// disclosure of exception details
        /// such as a stacktrace would be a
        /// security problem. Therefore,
        /// the failsafe error display
        /// does not describe the original
        /// exception but only the problem
        /// in the error page itself.</para>
        /// </remarks>
        public static void transfer(string vpath) {
            try {
                Server.Transfer(vpath);
            }
            catch (Exception e2) {
                Response.Clear();
                Response.ContentType = "text/plain";
                // report a bug in this error-handling code
                Response.Write(stringify(e2));
                Response.End();
            }
        }

        /// <summary>
        /// Render <paramref name="e" /> as a <c>string</c>.
        /// </summary>
        /// <remarks>
        /// <see cref="Exception.ToString" /> isn't a bad start, but some
        /// ASP.NET exceptions (notably <see cref="HttpCompileException" />)
        /// omit some useful details.
        /// </remarks>
        public static string stringify(Exception e) {
            if (e == null) return "(null)\n";
            StringBuilder sb = new StringBuilder();
            while (e != null) {
                HttpCompileException hce = e as HttpCompileException;
                if (hce != null) {
                    if (hce.Results != null) {
                        foreach (string line in hce.Results.Output) {
                            sb.AppendFormat("{0}\n", line);
                        }
                    }
                }
                HttpParseException hpe = e as HttpParseException;
                if (hpe != null) {
                    IDictionary pm = propertyMap(hpe);
                    pm["FileName"] = null;
                    sb.AppendFormat(
                        "{0}({1}): {2}\n",
                        Potpourri.coalesce(pm["FileName"], "(null filename)"),
                        Potpourri.coalesce(pm["Line"], "(null line)"),
                        Potpourri.coalesce(pm["Message"], "(null message)"));
                }
                // the following is dynamic because
                // the type in question is not available
                // on some platforms (e.g., Microsoft .NET).
                if ("System.Web.Compilation.CompilationException" == e.GetType().FullName) {
                    sb.Append(stripMonoCompilationExceptionProperties(e));
                }
                sb.Append(e);
                sb.Append("\n");
                e = e.InnerException;
            }
            return sb.ToString();
        }

        /// <summary>
        /// Reveal details Mono provides
        /// </summary>
        private static String stripMonoCompilationExceptionProperties(Exception e) {
            if (e == null) throw new ArgumentNullException();
            Type eType = e.GetType();
            if (e.GetType().FullName != "System.Web.Compilation.CompilationException") {
                throw new ArgumentException("My argument must be of Mono's type System.Web.Compilation.CompilationException.");
            }
            ListDictionary ld = propertyMap(e);
            StringBuilder sb = new StringBuilder();
            foreach (DictionaryEntry p in ld) {
                String pValue = null;
                if (p.Value == null) {
                    pValue = "(null)";
                }
                else if (p.Value is String) {
                    pValue = (String)p.Value;
                }
                else if (typeof(IEnumerable).IsAssignableFrom(p.Value.GetType())) {
                    pValue = Potpourri.Join(", ", (IEnumerable)p.Value);
                }
                else {
                    pValue = String.Format("{0} {1}", p.Value.GetType().FullName, p.Value.ToString());
                }
                sb.AppendFormat("{0}: {1}\n", p.Key, pValue);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Extract as many property values as possible,
        /// ignoring exceptions and indexed properties
        /// as we go.
        /// </summary>
        private static ListDictionary propertyMap(object o) {
            ListDictionary result = new ListDictionary();
            foreach (PropertyInfo pi in o.GetType().GetProperties()) {
                if (pi.GetIndexParameters().Length > 0) continue;
                try {
                    result.Add(pi.Name, pi.GetValue(o, emptyArgs));
                }
                catch {
                    // just take as much as you can get
                }
            }
            return result;
        }

        /// <summary>convenience property</summary>
        protected static HttpContext Context {
            get {
                return HttpContext.Current;
            }
        }

        /// <summary>convenience property</summary>
        protected static HttpServerUtility Server {
            get {
                return Context.Server;
            }
        }

        /// <summary>convenience property</summary>
        protected static HttpResponse Response {
            get {
                return Context.Response;
            }
        }

        /// <summary>convenience property</summary>
        protected static HttpRequest Request {
            get {
                return Context.Request;
            }
        }
    }

    /// <summary>
    /// Load and install error-handling code according to
    /// application configuration data.
    /// </summary>
    public class OopsnetHttpModule : IHttpModule, IDisposable {
        private HandleError handler;
        private HttpApplication app;

        /// <summary>
        /// Dispatch the exception to the error handler
        /// </summary>
        public void handleApplicationErrorImpl(object sender, EventArgs args) {
            Exception e = Server.GetLastError();
            handler(e);
        }
        /// <remarks>
        /// C# 1.0 is verbose
        /// </remarks>
        public EventHandler handleApplicationError;
        /// <summary>
        /// initialize <see cref="handleApplicationError" />
        /// </summary>
        public OopsnetHttpModule() {
            handleApplicationError = new EventHandler(handleApplicationErrorImpl);
        }

        /// <summary>
        /// Load config data and 
        /// register event handlers
        /// </summary>
        public void Init(HttpApplication app) {
            this.app = app;
            string handlerAQN = getRequiredConfigValue("seanfoy.oopsnet.assemblyQualifiedName");
            string handlerName = getRequiredConfigValue("seanfoy.oopsnet.handlerName");
            Type handlerType = Type.GetType(handlerAQN);
            if (handlerType == null) {
                throw makeConfigException(
                    String.Format(
                        "handler type {0} cannot be found.",
                        handlerAQN));
            }
            handler = getHandlerFromStaticField(handlerName, handlerType);
            if (handler == null) {
                handler = getHandlerFromStaticMethod(handlerName, handlerType);
            }
            if (handler == null) {
                handler = getHandlerFromInstanceMethod(handlerName, handlerType);
            }

            if (handler == null) {
                throw makeConfigException(
                    String.Format(
                        "Cannot find a static delegate field, a static method, or (if {0} has a no-argument constructor) an instance method named {1} on type {0}",
                        handlerType.FullName,
                        handlerName));
            }
            app.Error += handleApplicationError;
        }

        private static HandleError getHandlerFromStaticField(string handlerName, Type handlerType) {
            try {
                return (HandleError)handlerType.GetField(handlerName).GetValue(null);
            }
            catch (NullReferenceException) {
                return null;
            }
        }

        private static HandleError getHandlerFromStaticMethod(string handlerName, Type handlerType) {
            BindingFlags staticMethods = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
            MethodInfo mi = handlerType.GetMethod(handlerName, staticMethods);
            if (mi == null) return null;

            return
                (HandleError)Delegate.CreateDelegate(
                    typeof(HandleError),
                    mi);
        }

        private static HandleError getHandlerFromInstanceMethod(string handlerName, Type handlerType) {
            ConstructorInfo ci = handlerType.GetConstructor(emptyTypes);
            if (ci != null) {
                BindingFlags instanceMethods = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
                MethodInfo mi = handlerType.GetMethod(handlerName, instanceMethods);
                if (mi != null) {
                    object instance = ci.Invoke(emptyArgs);
                    return
                        (HandleError)Delegate.CreateDelegate(
                            typeof(HandleError),
                            instance,
                            mi.Name);
                }
            }
            return null;
        }

        private static object [] emptyArgs = new object [] {};
        private static Type [] emptyTypes = new Type [] {};

        /// <summary>
        /// Deregister event handlers
        /// </summary>
        public void Dispose() {
            if (app == null) return;
            app.Error -= handleApplicationError;
            app = null;
            GC.SuppressFinalize(this);
        }

        private string getRequiredConfigValue(string key) {
            string result = AppSettings[key];
            if (result != null) return result;
            throw makeConfigException(
                String.Format(
                    "{0} requires an appSetting with key: {1}",
                    this.GetType().FullName,
                    key));
        }

        private Exception makeConfigException(string reason) {
            return new
                #if NET_1_0 || NET_1_1
                ConfigurationException(
                #else
                Exception(
                #endif
                    reason);
        }

        /// <summary>convenience property</summary>
        protected static NameValueCollection AppSettings {
            get {
                return ASPNetErrorHandler.AppSettings;
            }
        }

        /// <summary>convenience property</summary>
        protected static HttpContext Context {
            get {
                return HttpContext.Current;
            }
        }

        /// <summary>convenience property</summary>
        protected static HttpServerUtility Server {
            get {
                return Context.Server;
            }
        }
    }

    public class SOAPErrorHandler {
        public static SoapException toSoapException(Exception e) {
            XmlDocument d = xmlize(e);
            return
                new SoapException(
                    e.Message,
                    SoapException.ServerFaultCode,
                    HttpContext.Current.Request.Url.ToString(),
                    d.DocumentElement,
                    e);
        }
        /// <remarks>It should not, but it does on Mono as
        /// of 22 June 2009.</remarks>
        /// <see cref="http://msdn.microsoft.com/en-us/library/system.web.services.protocols.soapexception.detail%28VS.71%29.aspx" />
        /// <see cref="http://support.microsoft.com/kb/833379" />
        /// <see cref="http://anonsvn.mono-project.com/viewvc/trunk/mcs/class/System.Web.Services/System.Web.Services.Protocols/Fault.cs?revision=79855&view=markup" />
        public static Boolean frameworkSuppliesDetailP() {
            return "true".Equals(ASPNetErrorHandler.AppSettings["seanfoy.oopsnet.frameworkSuppliesSoapFaultDetail"]);
        }
        public static void xmlizeHelper(XmlNode parent, Object e) {
            XmlDocument d2 = new XmlDocument();
            using (MemoryStream ms = new MemoryStream()) {
                SoapFormatter formatter = new SoapFormatter();
                formatter.Serialize(ms, e);
                ms.Seek(0, SeekOrigin.Begin);
                d2.Load(ms);
                XmlDocument ownerDocument = parent.OwnerDocument;
                if (ownerDocument == null) ownerDocument = (XmlDocument)parent;
                parent.AppendChild(ownerDocument.ImportNode(d2.DocumentElement, true));
            }
        }
        public static XmlDocument xmlize(Object e) {
            Type eType = e.GetType();
            if (!eType.IsSerializable) return null;
            XmlDocument doc = new XmlDocument();
            XmlNode parent =
                frameworkSuppliesDetailP() ?
                    doc :
                    doc.AppendChild(
                        doc.CreateElement(
                            SoapException.DetailElementName.Name,
                            SoapException.DetailElementName.Namespace));
            xmlizeHelper(parent, e);
            foreach (XmlNode n in doc.SelectNodes("//text()")) {
                //HACK:
                // null is outside the XML 1.1 Char repertoire
                // http://www.w3.org/TR/2006/REC-xml11-20060816/#charsets
                // I guess this is for the convenience of C programmers.
                // And maybe it's a justifiable compromise given XML's
                // text-y heritage. It's terribly inconvenient though
                // that .NET strings can contain null, that SoapFormatter
                // uses them gratuitously, and that XmlSerializer refuses
                // to serialize Exceptions due to declarative CAS
                // demands on certain members of System.Exception.
                n.InnerText = n.InnerText.Replace("\u0000", String.Empty);
            }
            return doc;
        }
        public static Exception dexmlize(XmlNode detail) {
            if (detail == null) return null;
            if (detail.SelectSingleNode("detail") != null) {
                throw new ArgumentException("This detail element appears to have been generated by a misconfigured oopsnet soap service. Is the appSetting seanfoy.oopsnet.frameworkSuppliesSoapFaultDetail set correctly for your runtime?: " + detail.OuterXml);
            }
            SoapFormatter formatter = new SoapFormatter();
            using (MemoryStream ms = new MemoryStream()) {
                //XmlWriter isn't IDisposable until 2.0
                XmlTextWriter w = new XmlTextWriter(ms, Encoding.UTF8);
                try {
                    detail.WriteContentTo(w);
                    w.Flush();
                    ms.Seek(0, SeekOrigin.Begin);
                    return (Exception)formatter.Deserialize(ms);
                }
                finally {
                    w.Close();
                }
            }
        }
        private static object [] emptyArgs = new object [] {};
    }

    public class WinFormsErrorHandler : IDisposable {
        public static void die() {
            Application.Exit();
        }
        public void handle(Exception e) {
            if (e != null) {
                Console.Error.WriteLine(e.ToString());
            }
            exitStatus = statusFor(e);
            die();
        }
        public void handle(Object sender, ThreadExceptionEventArgs args) {
            handle(args.Exception);
        }
        private int exitStatus;
        public virtual int statusFor(Exception e) {
            if (e == null) return 0;
            return 1;
        }
        public void exit(Object sender, EventArgs args) {
            Environment.Exit(exitStatus);
        }
        private ThreadExceptionEventHandler handler;
        private EventHandler exitHandler;
        public WinFormsErrorHandler() {
            handler = new ThreadExceptionEventHandler(handle);
            Application.ThreadException += handler;
            exitHandler = new EventHandler(exit);
            Application.ApplicationExit += exitHandler;
        }
        public void Dispose() {
            try {
                if (handler != null) {
                    Application.ThreadException -= handler;
                    handler = null;
                }
            }
            catch {
                //ignore
            }
            try {
                if (exitHandler != null) {
                    Application.ApplicationExit -= exitHandler;
                    handler = null;
                }
            }
            catch {
                //ignore
            }
            GC.SuppressFinalize(this);
        }
        private class Parens : IDisposable {
            protected object ch;
            protected WinFormsErrorHandler eh;
            public Parens() {
                ch = ConsoleErrorHandler.install();
                eh = new WinFormsErrorHandler();
            }
            private Boolean TryCleanup(IDisposable d) {
                if (d == null) return true;
                try {
                    d.Dispose();
                    return true;
                }
                catch {
                    return false;
                }
            }
            public void Dispose() {
                TryCleanup(eh);
                ConsoleErrorHandler.uninstall(ch);
            }
        }
        public static IDisposable install() {
            return new Parens();
        }
    }

    public static class Potpourri {
        public static String Join(String separator, IEnumerable a) {
            StringBuilder sb = new StringBuilder();
            foreach (Object i in (IEnumerable)a) {
                sb.AppendFormat("{0}{1}", i, separator);
            }
            TrimEnd(sb, separator);
            return sb.ToString();
        }

        public static void TrimEnd(StringBuilder sb, String waste) {
            if (waste == null || waste.Length == 0) return;
            if (sb.Length <= waste.Length) return;
            sb.Replace(waste, String.Empty, sb.Length - waste.Length, waste.Length);
        }

        public static Object coalesce(params object [] args) {
            if (Object.ReferenceEquals(args, null)) return null;
            foreach (object i in args) {
                if (!Object.ReferenceEquals(i, null)) return i;
            }
            return null;
        }

        public static object setDefault(IDictionary d, object k, MakeDefault v) {
            object result = d[k];
            if (result == null) {
                result = v();
                d[k] = result;
            }
            return result;
        }
        public delegate object MakeDefault();
    }
}
